package se.webbzon.oschi01.worldobject.appearance;

import java.awt.Color;
import java.awt.Container;
import java.awt.Font;
import java.awt.FontMetrics;

import javax.media.j3d.Appearance;
import javax.media.j3d.BoundingBox;
import javax.media.j3d.Font3D;
import javax.media.j3d.FontExtrusion;
import javax.media.j3d.Geometry;
import javax.media.j3d.Material;
import javax.media.j3d.RenderingAttributes;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Text3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.TransparencyAttributes;
import javax.vecmath.Color3f;
import javax.vecmath.Point3d;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3d;

import se.webbzon.oschi01.warning.Warning;

public class TextAppearance3D extends AbstractAppearance3D {
	
	// The layer separation coefficient (m/layer)
	private static double layerSeparation = 0.01;
	
	// Text appearance properties
	public static final int TRANSPARENCY		= (1<<0);
	public static final int LAYERS				= (1<<4);
	public static final int	ALIGN_LEFT			= (1<<5);
	public static final int	ALIGN_RIGHT			= (1<<6);
	public static final int ALIGN_BOTTOM		= (1<<7);
	public static final int ALIGN_TOP			= (1<<8);
	public static final int ALIGN_BACK			= (1<<9);
	public static final int ALIGN_FRONT			= (1<<10);
	public static final int KEEP_PROPORTIONS	= (1<<11);
	
	// The model used to present the text appearance
	private final Shape3D model;
	
	// The properties of this text appearance
	private final int properties;
	
	// The layer vector
	private final Vector3d layerVector;

	/*============================================================
	Constructors
	============================================================*/
	
	public TextAppearance3D(Font font, String text) {
		this(0,0,font,text);
	}
	
	/** Creates a new text appearance with a given rendering order and a set of
	 * text appearance properties. The text will be rendered with the given font
	 * and text. **/
	public TextAppearance3D(	int renderingOrder,
								int textAppearanceProperties,
								final Font font,
								final String text) {
		super(renderingOrder);
		properties = textAppearanceProperties;
		Font3D font3d = new Font3D(font,new FontExtrusion());
		Text3D text3d = new Text3D(font3d,text,new Point3f(),Text3D.ALIGN_FIRST,Text3D.PATH_RIGHT);
		text3d.setCapability(Text3D.ALLOW_STRING_READ);
		text3d.setCapability(Text3D.ALLOW_STRING_WRITE);
		model = new PrivateModel(textAppearanceProperties,text3d);

		// Create text transform group
		TransformGroup transformGroup = new TransformGroup();
		transformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_READ);
		transformGroup.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
		updateTransformGroup(transformGroup,text3d,properties);
		transformGroup.addChild(model);
		super.getTransformGroup().addChild(transformGroup);
		
		// Initialize layer vector
		if (isSet(textAppearanceProperties,LAYERS))
			layerVector = new Vector3d();
		else
			layerVector = null;
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Sets the layers separation coefficient. The default layer
	 * separation coefficient is 0.01 m/layer. **/
	public static void setLayerSeparation(double separation) {
		layerSeparation = separation;
	}
	
	/** Returns the layers separation coefficient. **/
	public static double getLayerSeparation() {
		return layerSeparation;
	}
	
	/** Sets the text of the the text appearance. **/
	public void setText(String text) {
		final Text3D text3d = (Text3D) model.getGeometry();
		if (!text3d.getString().equals(text)) {
			text3d.setString(text);
			final TransformGroup transformGroup = 
					(TransformGroup) getTransformGroup().getChild(0);
			updateTransformGroup(transformGroup,text3d,properties);
		}
	}
	
	/** Returns the text of the text appearance. **/
	public String getText() {
		return ((Text3D) model.getGeometry()).getString();
	}
	
	/** Writes the desired proportion of the text appearance to
	 * the given vector. **/
	public void getProportions(Vector3d proportions) {
		final BoundingBox boundary = new BoundingBox();
		((Text3D) model.getGeometry()).getBoundingBox(boundary);
		final Point3d point = new Point3d();
		boundary.getUpper(point);
		proportions.set(point);
		boundary.getLower(point);
		proportions.sub(point);
	}
	
	/** Sets the color of the text appearance. **/
	public void setColor(Color color) {
		Color3f c = new Color3f(color);
		model.getAppearance().getMaterial().setAmbientColor(c);
		model.getAppearance().getMaterial().setDiffuseColor(c);
	}
	
	/** Returns the color of this text appearance. **/
	public Color getColor() {
		Color3f c = new Color3f();
		model.getAppearance().getMaterial().getAmbientColor(c);
		return c.get();
	}
	
	/** Sets the transparency value of the text appearance. **/
	public void setTransparency(float transparency) {
		model.getAppearance().getTransparencyAttributes().setTransparency(transparency);
	}
	
	/** Returns the transparency value of the text appearance. **/
	public float getTransparency() {
		return model.getAppearance().getTransparencyAttributes().getTransparency();
	}
	
	/** Sets the rendering layer of this text appearance. **/
	public void setLayer(double xLayer, double yLayer, double zLayer) {
		layerVector.set(xLayer,yLayer,zLayer);
	}
	
	/** Returns the vector which describes the rendering layer of this 
	 * text appearance. **/
	public void getLayer(Vector3d layerVector) {
		this.layerVector.get(layerVector);
	}
	
	@Override public void setTransform(Transform3D transform) {
		if (layerVector != null) {
			final Vector3d translation = new Vector3d();
			transform.get(translation);
			translation.scaleAdd(layerSeparation,layerVector,translation);
			transform.setTranslation(translation);
		}
		super.setTransform(transform);
	}
	
	@Override public boolean getVisible() {
		return model.getAppearance().getRenderingAttributes().getVisible();
	}

	@Override public void setVisible(boolean visible) {
		model.getAppearance().getRenderingAttributes().setVisible(visible);
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Updates the transform of the given transform group so the given text3d instance
	 * of the group has a size smaller than or equal to 1x1x1 (m).
	 * the transform group will also translate the text3d instance in a proper 
	 * way given by the properties of the text appearance. **/
	@Warning(msg = 	"Temporary solution due to error in Java3D 1.5.2 when " +
					"calling method Text3D.getBoundingBox().")
	private static void updateTransformGroup(	TransformGroup transformGroup, 
												Text3D text3d, 
												int properties) {
		if (text3d.getString().isEmpty())
			return;
		
		// Calculate offset of the text
		final double kx, ky, kz;
		kx = isSet(properties,ALIGN_LEFT) ? 0 : 
				isSet(properties,ALIGN_RIGHT) ? -1 : -0.5;
		ky = isSet(properties,ALIGN_BOTTOM) ? 0 : 
				isSet(properties,ALIGN_TOP) ? -1 : -0.5;
		kz = isSet(properties,ALIGN_FRONT) ? 0 : 
				isSet(properties,ALIGN_BACK) ? -1 : -0.5;
		
		// Retrieve boundary of the text
		final BoundingBox boundary = new BoundingBox();
		text3d.getBoundingBox(boundary);
		final Point3d lower = new Point3d(), upper = new Point3d();
		boundary.getLower(lower);
		boundary.getUpper(upper);
		
		// Complement boundary data to ensure that the string 
		// fits inside the text appearance
		Container c = new Container();
		FontMetrics m = c.getFontMetrics(text3d.getFont3D().getFont());
		double width = m.stringWidth(text3d.getString());
		double scale = (upper.x - lower.x) / width;
		upper.y = m.getMaxAscent() * scale;
		lower.y = -m.getMaxDescent() * scale;
		
		// Test if the boundary box has zero volume
		if (lower.x == upper.x || lower.y == upper.y || lower.z == upper.z)
			return;
		
		// Scale and translate the text appearance
		final Vector3d scaling = new Vector3d(	1/(upper.x-lower.x),
									1/(upper.y-lower.y),
									1/(upper.z-lower.z));
		final Vector3d translation = new Vector3d(kx,ky-scaling.y * lower.y,kz);
		
		final Transform3D transform = new Transform3D();
		transform.setTranslation(translation);
		transform.setScale(scaling);
		transformGroup.setTransform(transform);
	}
	
	/** Returns true if the given property is set in the given properties. **/
	private static final boolean isSet(int properties, int property) {
		return ((properties & property) == property);
	}
	
	/*============================================================
	Private Classes
	============================================================*/
	
	private static class PrivateModel extends Shape3D {
		
		/*============================================================
		Constructors
		============================================================*/
		
		/** Creates a new model from a 3D geometry. **/
		public PrivateModel(int properties, Geometry model) {
			super(model);
			Appearance appearance = new Appearance();
			appearance.setCapability(Appearance.ALLOW_TEXTURE_WRITE);
			
			// Add material to the simple appearance
			Material mat = new Material();
			mat.setCapability(Material.ALLOW_COMPONENT_READ);
			mat.setCapability(Material.ALLOW_COMPONENT_WRITE);
			mat.setAmbientColor(new Color3f(0.9f,0.9f,0.9f));
			mat.setDiffuseColor(new Color3f(0.9f,0.9f,0.9f));
			mat.setSpecularColor(new Color3f(0.0f,0.0f,0.0f));
			mat.setEmissiveColor(new Color3f(0.0f,0.0f,0.0f));
			appearance.setMaterial(mat);
			
			// Add rendering attributes to the simple appearance
			RenderingAttributes ra = new RenderingAttributes();
			ra.setCapability(RenderingAttributes.ALLOW_VISIBLE_READ);
			ra.setCapability(RenderingAttributes.ALLOW_VISIBLE_WRITE);
			ra.setAlphaTestFunction(RenderingAttributes.NOT_EQUAL);
			ra.setAlphaTestValue(0.0f);
			appearance.setRenderingAttributes(ra);
			
			if (isSet(properties,TRANSPARENCY)) {
				// Add transparency attributes to the simple appearance
				TransparencyAttributes tra = new TransparencyAttributes();
				tra.setTransparencyMode(TransparencyAttributes.BLENDED);
				tra.setTransparency(0.0f);
				appearance.setTransparencyAttributes(tra);
			}
			
			setAppearance(appearance);
		}
		
	}

}
